﻿using System;
using UnityEngine;

namespace Nomo.UnityCoreModule
{
    /// <summary>
    /// 提供一些适用于 Unity 的数学方法。
    /// </summary>
    public static unsafe class NomoMath
    {
        private const           int MaxStackLengthAllowed                       = 1024;
        private static readonly int MaxVector3ArrayLengthAllowedAllocateOnStack = MaxStackLengthAllowed / sizeof(Vector3);
        private static readonly int MaxVector2ArrayLengthAllowedAllocateOnStack = MaxStackLengthAllowed / sizeof(Vector2);

        /// <summary>
        /// 返回一个值，这个值有 <paramref name="probability"/> 的概率为 <see langword="true"/> ， 有 (1 - <paramref name="probability"/>) 的概率为 <see langword="false"/> .
        /// </summary>
        /// <param name="probability">概率。</param>
        /// <returns>根据传入的概率返回一个布尔值。</returns>
        public static bool P(float probability)
        {
            return probability > UnityEngine.Random.value || probability == 1F;
        }

        /// <summary>
        /// 返回一个值，这个值有 0.5 的概率为 <see langword="true"/> ， 有 0.5 的概率为 <see langword="false"/> .
        /// </summary>
        /// <returns>返回一个布尔值。</returns>
        public static bool FiftyFifty()
        {
            return 0.5F > UnityEngine.Random.value;
        }

        /// <summary>
        /// 贝塞尔插值。
        /// </summary>
        /// <param name="points">包含有全部基准点的数组。</param>
        /// <param name="t">插值。它的取值范围为 0 到 1 .</param>
        /// <returns>根据基准点和插值得到的结果。如果 <paramref name="points"/> 的长度为 0 ，则会返回默认值。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="points"/> 为 <see langword="null"/> .</exception>
        public static Vector3 Blerp(Vector3[] points, float t)
        {
            return Blerp(points, t, 0, points.Length);
        }

        /// <summary>
        /// 贝塞尔插值。
        /// </summary>
        /// <param name="points">包含有基准点的数组。</param>
        /// <param name="t">插值。它的取值范围为 0 到 1 .</param>
        /// <param name="startIndex">第一个基准点在 <paramref name="points"/> 中的索引。</param>
        /// <param name="count">基准点的个数。</param>
        /// <returns>根据基准点和插值得到的结果。如果 <paramref name="count"/> 为 0 ，则会返回默认值。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="points"/> 为 <see langword="null"/> .</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex"/> 或 <paramref name="count"/> 小于 0 .</exception>
        /// <exception cref="ArgumentException"><paramref name="startIndex"/> 与 <paramref name="count"/> 之和大于 <paramref name="points"/> 的长度。</exception>
        public static Vector3 Blerp(Vector3[] points, float t, int startIndex, int count)
        {
            if (points == null)
            {
                throw new ArgumentNullException(nameof(points));
            }
            if (startIndex < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(startIndex));
            }
            if (count < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(count));
            }
            if (startIndex + count > points.Length)
            {
                throw new ArgumentException();
            }
            if (count == 0)
            {
                return default;
            }
            fixed (Vector3* ptr = points)
            {
                var ptrAtStartIndex = ptr + startIndex;
                switch (count)
                {
                    case 1:
                        return *ptrAtStartIndex;
                    case 2:
                        return Vector3.Lerp(*ptrAtStartIndex, *(ptrAtStartIndex + 1), t);
                    default:
                        return BlerpInternal(ptrAtStartIndex, t, count);
                }
            }
        }

        private static Vector3 BlerpInternal(Vector3* points, float t, int count)
        {
            return count <= MaxVector3ArrayLengthAllowedAllocateOnStack ? BlerpStackAllocInternal(points, t, count) : BlerpHeapAllocInternal(points, t, count);
        }

        private static Vector3 BlerpStackAllocInternal(Vector3* points, float t, int count)
        {
            var array = stackalloc Vector3[count - 1];
            for (var i = 0; i != count - 1; i++)
            {
                array[i] = Vector3.Lerp(points[i], points[i + 1], t);
            }
            while (--count != 0)
            {
                for (var i = 0; i != count - 1; i++)
                {
                    array[i] = Vector3.Lerp(array[i], array[i + 1], t);
                }
            }
            return *array;
        }

        private static Vector3 BlerpHeapAllocInternal(Vector3* points, float t, int count)
        {
            var heapArray = new Vector3[count - 1];
            fixed (Vector3* array = heapArray)
            {
                for (var i = 0; i != count - 1; i++)
                {
                    array[i] = Vector3.Lerp(points[i], points[i + 1], t);
                }
                while (--count != 0)
                {
                    for (var i = 0; i != count - 1; i++)
                    {
                        array[i] = Vector3.Lerp(array[i], array[i + 1], t);
                    }
                }
                return *array;
            }
        }

        /// <summary>
        /// 贝塞尔插值。
        /// </summary>
        /// <param name="points">包含有全部基准点的数组。</param>
        /// <param name="t">插值。它的取值范围为 0 到 1 .</param>
        /// <returns>根据基准点和插值得到的结果。如果 <paramref name="points"/> 的长度为 0 ，则会返回默认值。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="points"/> 为 <see langword="null"/> .</exception>
        public static Vector2 Blerp(Vector2[] points, float t)
        {
            return Blerp(points, t, 0, points.Length);
        }

        /// <summary>
        /// 贝塞尔插值。
        /// </summary>
        /// <param name="points">包含有基准点的数组。</param>
        /// <param name="t">插值。它的取值范围为 0 到 1 .</param>
        /// <param name="startIndex">第一个基准点在 <paramref name="points"/> 中的索引。</param>
        /// <param name="count">基准点的个数。</param>
        /// <returns>根据基准点和插值得到的结果。如果 <paramref name="count"/> 为 0 ，则会返回默认值。</returns>
        /// <exception cref="ArgumentNullException"><paramref name="points"/> 为 <see langword="null"/> .</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex"/> 或 <paramref name="count"/> 小于 0 .</exception>
        /// <exception cref="ArgumentException"><paramref name="startIndex"/> 与 <paramref name="count"/> 之和大于 <paramref name="points"/> 的长度。</exception>
        public static Vector2 Blerp(Vector2[] points, float t, int startIndex, int count)
        {
            if (points == null)
            {
                throw new ArgumentNullException(nameof(points));
            }
            if (startIndex < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(startIndex));
            }
            if (count < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(count));
            }
            if (startIndex + count > points.Length)
            {
                throw new ArgumentException();
            }
            if (count == 0)
            {
                return default;
            }
            fixed (Vector2* ptr = points)
            {
                var ptrAtStartIndex = ptr + startIndex;
                switch (count)
                {
                    case 1:
                        return *ptrAtStartIndex;
                    case 2:
                        return Vector2.Lerp(*ptrAtStartIndex, *(ptrAtStartIndex + 1), t);
                    default:
                        return BlerpInternal(ptrAtStartIndex, t, count);
                }
            }
        }

        private static Vector2 BlerpInternal(Vector2* points, float t, int count)
        {
            return count <= MaxVector2ArrayLengthAllowedAllocateOnStack ? BlerpStackAllocInternal(points, t, count) : BlerpHeapAllocInternal(points, t, count);
        }

        private static Vector2 BlerpStackAllocInternal(Vector2* points, float t, int count)
        {
            var array = stackalloc Vector2[count - 1];
            for (var i = 0; i != count - 1; i++)
            {
                array[i] = Vector2.Lerp(points[i], points[i + 1], t);
            }
            while (--count != 0)
            {
                for (var i = 0; i != count - 1; i++)
                {
                    array[i] = Vector2.Lerp(array[i], array[i + 1], t);
                }
            }
            return *array;
        }

        private static Vector2 BlerpHeapAllocInternal(Vector2* points, float t, int count)
        {
            var heapArray = new Vector2[count - 1];
            fixed (Vector2* array = heapArray)
            {
                for (var i = 0; i != count - 1; i++)
                {
                    array[i] = Vector2.Lerp(points[i], points[i + 1], t);
                }
                while (--count != 0)
                {
                    for (var i = 0; i != count - 1; i++)
                    {
                        array[i] = Vector2.Lerp(array[i], array[i + 1], t);
                    }
                }
                return *array;
            }
        }

        /// <summary>
        /// 返回一个值，这个值指示直线 <paramref name="p1"/><paramref name="p2"/> 和 直线 <paramref name="p3"/><paramref name="p4"/> 是否相交。
        /// </summary>
        /// <param name="p1">第 1 条直线的第 1 个点。</param>
        /// <param name="p2">第 1 条直线的第 2 个点。</param>
        /// <param name="p3">第 2 条直线的第 1 个点。</param>
        /// <param name="p4">第 2 条直线的第 2 个点。</param>
        /// <param name="result">如果相交则为交点，否则为默认值。</param>
        /// <returns>直线 <paramref name="p1"/><paramref name="p2"/> 和 直线 <paramref name="p3"/><paramref name="p4"/> 相交吗？</returns>
        public static bool LineIntersection(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, out Vector2 result)
        {
            result = default;

            var bx                 = p2.x    - p1.x;
            var by                 = p2.y    - p1.y;
            var dx                 = p4.x    - p3.x;
            var dy                 = p4.y    - p3.y;
            var bDotDPerpendicular = bx * dy - by * dx;
            // p1p2 ∥ p3p4
            if (bDotDPerpendicular == 0F)
            {
                result = default;
                return false;
            }

            var cx = p3.x - p1.x;
            var cy = p3.y - p1.y;
            var t  = (cx * dy - cy * dx) / bDotDPerpendicular;
            result.x = p1.x + t * bx;
            result.y = p1.y + t * by;
            return true;
        }

        /// <summary>
        /// 返回一个值，这个值指示线段 <paramref name="p1"/><paramref name="p2"/> 和 线段 <paramref name="p3"/><paramref name="p4"/> 是否相交。
        /// </summary>
        /// <param name="p1">第 1 条线段的第 1 个点。</param>
        /// <param name="p2">第 1 条线段的第 2 个点。</param>
        /// <param name="p3">第 2 条线段的第 1 个点。</param>
        /// <param name="p4">第 2 条线段的第 2 个点。</param>
        /// <param name="result">如果相交则为交点，否则为默认值。</param>
        /// <returns>线段 <paramref name="p1"/><paramref name="p2"/> 和 线段 <paramref name="p3"/><paramref name="p4"/> 相交吗？</returns>
        public static bool LineSegmentIntersection(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, out Vector2 result)
        {
            result = default;

            var bx                 = p2.x    - p1.x;
            var by                 = p2.y    - p1.y;
            var dx                 = p4.x    - p3.x;
            var dy                 = p4.y    - p3.y;
            var bDotDPerpendicular = bx * dy - by * dx;
            // p1p2 ∥ p3p4（平行线没有交点）
            if (bDotDPerpendicular == 0F)
            {
                return false;
            }

            var cx = p3.x - p1.x;
            var cy = p3.y - p1.y;
            var t  = (cx * dy - cy * dx) / bDotDPerpendicular;
            // 交点不在线段 p3p4 上
            if (t < 0F || t > 1F)
            {
                return false;
            }

            var u = (cx * by - cy * bx) / bDotDPerpendicular;
            // 交点不在线段 p1p2 上
            if (u < 0F || u > 1F)
            {
                return false;
            }

            result.x = p1.x + t * bx;
            result.y = p1.y + t * by;
            return true;
        }
    }
}
